﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Synchronization.Files;
using Microsoft.Synchronization.SimpleProviders;
using Microsoft.WindowsAzure.StorageClient;

namespace Microsoft.WindowsAzure.Accelerator.Synchronization
{
    /// <summary>
    /// Azure blob storage location information and interaction class.
    /// </summary>
    public class CloudSyncStore
    {
        public const UInt32 MaxFileNameLength = 255;
        public const UInt32 ETagLength = 16;
        
        /// <summary>
        /// Initializes a new instance of the <see cref="CloudSyncStore"/> class.
        /// </summary>
        /// <param name="cloudDirectory">The target directory.</param>
        /// <param name="storageAccount">The storage account.</param>
        public CloudSyncStore(String cloudDirectory, CloudStorageAccount storageAccount)
        {
            VarianceCheck = VarianceCheck.LastModifiedUtc;
            CloudClient = storageAccount.CreateCloudBlobClient();
            CloudDirectory = CloudClient.GetBlobDirectoryReference(cloudDirectory);
            CreateContainer();
        }

        /// <summary>
        /// Gets the client for accessing the Windows Azure Blob service.
        /// </summary>
        private CloudBlobClient CloudClient 
        { 
            get; 
            set; 
        }

        /// <summary>
        /// Gets the container in the Windows Azure Blob service.
        /// </summary>
        private CloudBlobContainer CloudContainer
        {
            get { return this.CloudDirectory.Container; }
        }

        /// <summary>
        /// Represents a virtual directory of blobs, designated by a delimiter character.
        /// </summary>
        private CloudBlobDirectory CloudDirectory
        {
            get; 
            set;
        }

        /// <summary>
        /// Strategy for change and concurrency testing.
        /// </summary>
        public VarianceCheck VarianceCheck
        {
            get; 
            set;
        }

        /// <summary>
        /// Creates an Azure blob storage container.
        /// </summary>
        /// <remarks>
        /// Azure Blob Storage requires some delay between deleting and recreating containers.  This loop 
        /// compensates for that delay if the first attempt to creat the container fails then wait 30 
        /// seconds and try again.  While the service actually returns error 409 with the correct description
        /// of the failure the storage client interprets that as the container existing and the
        /// call to CreateContainer returns false.  For now just work around this by attempting to create
        /// a test blob in the container bofore proceeding.
        /// </remarks>
        private void CreateContainer()
        {
            CloudContainer.CreateContainer(BlobContainerPublicAccessType.Off);
        }
        
        /// <summary>
        /// Traverses the files in the metadata store building up required information to detect changes.
        /// </summary>
        /// <remarks>
        /// Called by CloudSyncProvider.EnumerateItems.
        /// </remarks>
        /// <returns></returns>
        internal List<ItemFieldDictionary> ListBlobs()
        {
            var options = new BlobRequestOptions
                              {
                                  UseFlatBlobListing = true,
                                  BlobListingDetails = BlobListingDetails.Metadata
                              };
            var items = CloudDirectory.ListBlobs(options).OfType<CloudBlockBlob>().Select(csb => new CloudSyncBlob(csb, CloudDirectory)).Select(csb =>
                                                                                         new ItemFieldDictionary
                                                                                         {
                                                                                            new ItemField(CloudSyncItemFields.Name, typeof(String), csb.RelativePath),
                                                                                            new ItemField(CloudSyncItemFields.Timestamp, typeof(UInt64), (UInt64)csb.LastWriteTime.Ticks)
                                                                                         }).ToList<ItemFieldDictionary>();
            Trace.TraceInformation("Enumerate | Finished loaded blob data...");
            return items;
        }

        /// <summary>
        /// Creates a file retriever for the blob.
        /// </summary>
        /// <param name="attributes">The attributes.</param>
        /// <returns></returns>
        internal CloudSyncFileRetriever GetFileRetriever(CloudSyncAttributes attributes)
        {
            String tempFileName = System.IO.Path.GetTempFileName();
            Trace.TraceInformation(@"Getting File Retriever:  ""{0}"" -(d)-> ""{1}""", attributes.Name, tempFileName);
            var csb = new CloudSyncBlob(attributes.Name, CloudDirectory);
            try
            {
                csb.Get(tempFileName, AccessCondition.IfNotModifiedSince(attributes.LastModifiedUtc));
                FileStream fs = (!csb.IsMetaDirectory) ? File.OpenRead(tempFileName) : null;
                return new CloudSyncFileRetriever(csb.FileData, csb.RelativePath, fs);
            }
            catch (StorageException ex)
            {
                if (ex.ErrorCode == StorageErrorCode.BlobNotFound || ex.ErrorCode == StorageErrorCode.ResourceNotFound)
                {
                    throw new ApplicationException("Concurrency Variance:  blob download aborted; blob not found.", ex);
                }                
            }
            return null;
        }
       
        /// <summary>
        /// Creates the file.
        /// </summary>
        /// <remarks>
        /// Called by CloudSyncProvider.InsertItem.
        /// </remarks>
        /// <param name="fileData">The file data.</param>
        /// <param name="relativePath">The relative path.</param>
        /// <param name="dataStream">The data stream.</param>
        /// <returns></returns>
        internal CloudSyncAttributes InsertFile(FileData fileData, String relativePath, Stream dataStream)
        {
            if (fileData.Name.Length > MaxFileNameLength)
            {
                throw new ApplicationException("Storage Variance:  blob insert aborted; name too long.");
            }
            var csb = new CloudSyncBlob(fileData.RelativePath.SetPathChar('/'), CloudDirectory);
            try
            {
                csb.FileData = fileData;
                csb.RelativePath = relativePath;
                csb.Save(dataStream, AccessCondition.IfNoneMatch("*"));
            }
            catch (StorageException e)
            {
                if (e.ErrorCode == StorageErrorCode.BlobAlreadyExists|| e.ErrorCode == StorageErrorCode.ConditionFailed || e.ErrorCode == StorageErrorCode.ResourceAlreadyExists)
                {
                    throw new ApplicationException("Concurrency Variance:  blob insert aborted; blob already found.", e);
                }
                throw;
            }
            return new CloudSyncAttributes(csb.RelativePath, csb.LastWriteTime, csb.Blob.Properties.ETag);
        }

        /// <summary>
        /// Updates a blob using the provided file stream.
        /// </summary>
        /// <param name="attributes">The attributes.</param>
        /// <param name="fileData">The file data.</param>
        /// <param name="relativePath">The relative path.</param>
        /// <param name="dataStream">The data stream.</param>
        /// <returns>A collection of attributes.</returns>
        internal CloudSyncAttributes UpdateFile(CloudSyncAttributes attributes, FileData fileData, String relativePath, Stream dataStream)
        {
            var csb = new CloudSyncBlob(attributes.Name, CloudDirectory);
            try
            {
                csb.Blob.FetchAttributes(GetMetadataOptions());
                csb.FileData = fileData;
                csb.RelativePath = relativePath;
                csb.Save(dataStream, GetAccessCondition(null, attributes.LastModifiedUtc));
            }
            catch (StorageException e)
            {
                if (e.ErrorCode == StorageErrorCode.BlobNotFound || e.ErrorCode == StorageErrorCode.ResourceNotFound)
                {
                    throw new ApplicationException("Concurrency Variance:  blob update aborted; blob not found.", e);
                }
                if (e.ErrorCode == StorageErrorCode.ConditionFailed)
                {
                    throw new ApplicationException("Concurrency Variance:  blob updatge aborted; existing changed blob found.", e);
                }
            }            
            return new CloudSyncAttributes(csb.RelativePath, csb.LastWriteTime.ToUniversalTime(), csb.Properties.ETag);
        }

        /// <summary>
        /// Deletes the file.
        /// </summary>
        /// <param name="attributes">The attributes.</param>
        internal void DeleteFile(CloudSyncAttributes attributes)
        {
            var csb = new CloudSyncBlob(attributes.Name, CloudDirectory);
            try
            {
                csb.Blob.FetchAttributes(GetMetadataOptions());
                csb.Delete(GetAccessCondition(attributes));
            }
            catch (StorageClientException e)
            {
                if (e.ErrorCode == StorageErrorCode.BlobNotFound || e.ErrorCode == StorageErrorCode.ResourceNotFound)
                {
                    throw new ApplicationException("Concurrency Variance:  blob delete aborted; blob does not exist.");
                }
                if (e.ErrorCode == StorageErrorCode.ConditionFailed)
                {
                    throw new ApplicationException("Concurrency Variance:  blob delete aborted; blob has changed since last update.", e);
                }
                throw;
            }
         }

        /// <summary>
        /// Gets the access condition for concurrency checking.
        /// </summary>
        /// <param name="attributes">The cloud synchronization attributes.</param>
        /// <returns></returns>
        private AccessCondition GetAccessCondition(CloudSyncAttributes attributes)
        {
            return GetAccessCondition(
                null,
                attributes.LastModifiedUtc);
        }

        /// <summary>
        /// Gets the metadata options.
        /// </summary>
        /// <returns></returns>
        private BlobRequestOptions GetMetadataOptions()
        {
            return new BlobRequestOptions()
            {
                BlobListingDetails = BlobListingDetails.Metadata
            };
        }

        /// <summary>
        /// Gets the metadata options.
        /// </summary>
        /// <param name="condition">The condition.</param>
        /// <returns></returns>
        private BlobRequestOptions GetMetadataOptions(AccessCondition condition)
        {
            return new BlobRequestOptions
            {
                AccessCondition = condition,
                BlobListingDetails = BlobListingDetails.Metadata
            };
        }

        /// <summary>
        /// Gets the access condition for concurrency checking.
        /// </summary>
        /// <param name="eTag">The eTag required.</param>
        /// <param name="lastModifiedUtc">The last modified UTC.</param>
        /// <returns></returns>
        private AccessCondition GetAccessCondition(String eTag, DateTime lastModifiedUtc)
        {
            if (VarianceCheck == VarianceCheck.eTag && !String.IsNullOrEmpty(eTag))
                return AccessCondition.IfMatch(eTag);
            else if (VarianceCheck == VarianceCheck.LastModifiedUtc)
                return AccessCondition.IfNotModifiedSince(lastModifiedUtc);
            return AccessCondition.None;
        }

        /// <summary>
        /// Get the root of the blob container.
        /// </summary>
        /// <returns></returns>
        public String GetRootPath()
        {
            return CloudDirectory.Uri.AbsoluteUri;
        }
    }
}